import pandas as pd
import fbprophet
from concurrent.futures import ProcessPoolExecutor, as_completed
import time

# Prophet模型的参数经过网格搜索得到的最佳参数为：
# seasonality_prior_scale=0.001
# holidays_prior_scale=0.1
# changepoint_prior_scale=0.01
# seasonality_mode='additive'
# 网格搜索分为两次进行，均在训练集上以0.85:0.15的比例划分训练集与验证集，以预测结果在验证集上的wmape来排名
# 网格搜索的单次处理逻辑（数据预处理、特征处理等）与以下代码相同
# 第一次搜索范围为：
# changepoint_prior_scale: [0.001, 0.01, 0.1, 0.5]
# seasonality_prior_scale: [0.01, 0.1, 1.0, 10.0]
# 其他参数固定（holidays_prior_scale=10，seasonality_mode='additive'）
# 最佳结果为：changepoint_prior_scale=0.001，seasonality_prior_scale=0.01
# 第二次搜索范围为：
# holidays_prior_scale: [0.001, 0.01, 0.1, 1.0, 10.0, 20.0]
# seasonality_mode: ['additive', 'multiplicative']
# 其他参数固定（changepoint_prior_scale=0.001，seasonality_prior_scale=0.01，即第一次网搜索的最佳参数）
# 最佳结果为：holidays_prior_scale=0.1，seasonality_mode='additive'


def predictor(data, period, holiday):
    """
    Prophet算法预测模型
    :param data: 商品销量数据，包含日期('ds')、销量('y')
    :param period: 预测时期长度
    :return: 商品销量预测数据，包含日期('ds')、预测销量('yhat')
    """
    # 模型构建
    m = fbprophet.Prophet(interval_width=0.95,  # 预测置信区间的置信度
                          weekly_seasonality=True,  # 加入周季节性趋势
                          seasonality_prior_scale=0.001,  # 季节性组件的强度
                          holidays_prior_scale=0.1,  # 节假日模型组件的强度
                          changepoint_prior_scale=0.01,  # 调节“change point”选择的灵活度
                          seasonality_mode='additive',  # 加法季节性
                          holidays=holiday
                          )

    # 依据日期数加入趋势项
    days = len(data)
    if days > 240:
        m.add_seasonality(name='quarterly', period=91.25, fourier_order=8, mode='additive')  # 每季度
    if days > 60:
        m.add_seasonality(name='monthly', period=30.5, fourier_order=5, mode='additive')  # 每月

    # 拟合数据并预测
    m.fit(data)
    future = m.make_future_dataframe(periods=period)
    forecast = m.predict(future)
    forecast['yhat'] = forecast['yhat'].apply(lambda x: (0 if x < 0 else x))  # 对于小于0的不合理预测销量归零
    return forecast[['ds', 'yhat']]


def wmape_operator(predict_data, check_data):
    """
    计算wmape1、wmape2，即wmape的分子和分母
    :param predict_data: 预测数据集，包含日期('ds')、预测销量('yhat')
    :param check_data: 验证数据集
    :return: wmape1、wmape2值
    """
    wmape1 = 0
    wmape2 = 0
    for i in check_data.iterrows():
        bias = abs(float(predict_data.loc[predict_data['ds'] == i[1].ds]['yhat']) - i[1].y)
        wmape1 += bias
        wmape2 += i[1].y
    return wmape1, wmape2


if __name__ == '__main__':
    t1 = time.time()
    # 导入数据，并修改日期、商品ID、是否促销列的数据格式
    train_data = pd.read_csv('train.csv', dtype={'sale_date': str, 'goodsid': int, 'is_pro': int})
    test_data = pd.read_csv('test.csv', dtype={'sale_date': str, 'goodsid': int, 'is_pro': int})
    goods_info = pd.read_csv('goods_ch.csv',
                             usecols=['goodsid', 'div_id', 'catg_l_id',
                                      'catg_m_id', 'catg_s_id', 'season_class'])
    train_data['sale_date'] = pd.to_datetime(train_data['sale_date'], format='%Y%m%d')
    test_data['sale_date'] = pd.to_datetime(test_data['sale_date'], format='%Y%m%d')
    date_ch = pd.read_csv('date_ch.csv', encoding='gbk')
    # 节假日处理，法定假日全选，其他节假日只选择影响力较大的节假日
    holiday1 = date_ch[['dim_date_id', 'official_holiday_name']].dropna().reset_index(drop=True)
    holiday1.columns = ['ds', 'holiday']
    holiday2 = date_ch[['dim_date_id', 'festival_name']].dropna().reset_index(drop=True)
    holiday2.columns = ['ds', 'holiday']
    holiday_plus_ls = ['情人节', '元宵节', '七夕节', '教师节', '圣诞节', '平安夜']
    holiday2 = holiday2[holiday2['holiday'].isin(holiday_plus_ls)].reset_index(drop=True)
    holiday = pd.concat([holiday1, holiday2], ignore_index=True).sort_values('ds')
    holiday['ds'] = pd.to_datetime(holiday['ds'], format='%Y%m%d')

    # 利用多进程对每个商品进行趋势预测
    with ProcessPoolExecutor(max_workers=3) as executor:
        wmape1 = 0
        wmape2 = 0
        futures = {}
        for gid in goods_info['goodsid']:
            # 取出对应商品的训练集数据
            goods_data = train_data.loc[train_data['goodsid'] == gid][['sale_date', 'sales_qty']]
            goods_data = goods_data.reset_index(drop=True)
            goods_data.columns = ['ds', 'y']
            # 消除异常值(异常高的销量修改为None)
            mean = goods_data['y'].mean()
            std_15 = goods_data['y'].std() * 15
            goods_data.loc[goods_data['y'] > mean + std_15, 'y'] = None
            # 补全销量为0的日期销量数据
            start = goods_data['ds'].min()
            end = test_data[test_data['goodsid'] == gid]['sale_date'].min()
            goods_data = goods_data.set_index('ds')
            all_date = pd.date_range(start=start, end=end)[:-1]
            goods_data = goods_data.reindex(all_date, fill_value=0)
            goods_data['ds'] = goods_data.index
            goods_data = goods_data.reset_index(drop=True)
            # 计算预测日期长度
            pstart = test_data[test_data['goodsid'] == gid]['sale_date'].min()
            pend = test_data[test_data['goodsid'] == gid]['sale_date'].max()
            period = (pend - pstart).days + 1
            # 放入进程池，并记录相应的商品编号
            job = executor.submit(predictor, goods_data, period, holiday)
            futures[job] = gid
        # 将预测数据分为训练集和测试集分别保存
        prophet_predict_train = pd.DataFrame({'ds': [], 'yhat': [], 'goodsid': []})
        prophet_predict_test = pd.DataFrame({'ds': [], 'yhat': [], 'goodsid': []})
        for job in as_completed(futures):
            predict_data = job.result()
            gid = futures[job]
            # 每个商品训练集部分的趋势预测数据
            goods_train = train_data[train_data['goodsid'] == gid][['sale_date', 'goodsid']].reset_index(drop=True)
            goods_train.columns = ['ds', 'goodsid']
            goods_train = pd.merge(predict_data, goods_train, on='ds', how='right')
            prophet_predict_train = pd.concat([goods_train, prophet_predict_train]).reset_index(drop=True)
            # 每个商品测试集部分的趋势预测数据
            goods_test = test_data[test_data['goodsid'] == gid][['sale_date', 'goodsid']].reset_index(drop=True)
            goods_test.columns = ['ds', 'goodsid']
            goods_test = pd.merge(predict_data, goods_test, on='ds', how='right')
            prophet_predict_test = pd.concat([goods_test, prophet_predict_test]).reset_index(drop=True)
        # 将预测结果导出
        prophet_predict_train.to_csv('prophet_predict_train.csv', index=False)
        prophet_predict_test.to_csv('prophet_predict_test.csv', index=False)
    print('共耗时: {}'.format(time.time() - t1))
